The 1/n family#
We play with the \(1/n\) portfolio. We start with a vanilla implementation using daily rebalancing. Every portfolio should be the solution of a convex optimization problem, see https://www.linkedin.com/pulse/stock-picking-convex-programs-thomas-schmelzer. We do that and show methods to construct the portfolio with
the minimization of the Euclidean norm of the weights.
the minimization of the \(\infty\) norm of the weights.
and the maximization of the Entropy of the weights.
the minimization of the tracking error to an \(1/n\) portfolio.
We also play with sparse updates, e.g. rather than rebalancing daily, we act only once the deviation of our drifted portfolio got too large from the target \(1/n\) portfolio.
This problem has been discussed https://www.linkedin.com/feed/update/urn:li:activity:7149432321388064768/
import pandas as pd
import numpy as np
from cvx.simulator import Builder
# load prices from flat csv file
prices = pd.read_csv("data/stock-prices.csv", header=0, index_col=0, parse_dates=True)
# Implement the 1/n portfolio using the Builder
builder = Builder(prices=prices, initial_aum=1e6)
for _, state in builder:
assets = state.assets
n = len(assets)
builder.weights = np.ones(n)/n
# it's important to also set the aum after setting the weights
# Here one could apply trading costs
# Access via state.trades, etc.
builder.aum = state.aum
portfolio = builder.build()
portfolio.snapshot(aggregate=True)
With cvxpy#
Formulating the problem above as a convex program is most useful when additional constraints have to be reflected. It also helps to link the 1/n portfolio to Tikhonov regularization and interpret its solution as a cornercase for more complex portfolios we are building
import cvxpy as cp
Minimization of the Euclidean norm#
We minimize the Euclidean norm of the weight vector. Same results as above but with opten door to the world of convex paradise.
builder = Builder(prices=prices, initial_aum=1e6)
for _, state in builder:
assets = state.assets
n = len(assets)
weights = cp.Variable(n)
objective = cp.norm(weights, 2)
constraints = [weights >= 0, cp.sum(weights) == 1]
# we are using the new CLARABEL solver
cp.Problem(objective=cp.Minimize(objective), constraints=constraints).solve(solver=cp.CLARABEL)
# update weights & aum as before
builder.weights = weights.value
builder.aum = state.aum
portfolio = builder.build()
portfolio.snapshot(aggregate=True)
Minimization of the \(\infty\) norm#
Based on an idea by Vladimir Markov
builder = Builder(prices=prices, initial_aum=1e6)
for _, state in builder:
assets = state.assets
n = len(assets)
weights = cp.Variable(n)
objective = cp.norm_inf(weights)
constraints = [weights >= 0, cp.sum(weights) == 1]
# we are using the new CLARABEL solver
cp.Problem(objective=cp.Minimize(objective), constraints=constraints).solve(solver=cp.CLARABEL)
# update weights & aum as before
builder.weights = weights.value
builder.aum = state.aum
portfolio = builder.build()
portfolio.snapshot(aggreagate=True)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[7], line 16
13 builder.aum = state.aum
15 portfolio = builder.build()
---> 16 portfolio.snapshot(aggreagate=True)
TypeError: Portfolio.snapshot() got an unexpected keyword argument 'aggreagate'
Maximization of the entropy#
One can also maximize the entropy to arrive at the same result
builder = Builder(prices=prices, initial_aum=1e6)
for _, state in builder:
assets = state.assets
n = len(assets)
weights = cp.Variable(n)
objective = cp.sum(cp.entr(weights))
constraints = [weights >= 0, cp.sum(weights) == 1]
cp.Problem(objective=cp.Maximize(objective), constraints=constraints).solve(solver=cp.CLARABEL)
# update weights & aum as before
builder.weights = weights.value
builder.aum = state.aum
portfolio = builder.build()
portfolio.snapshot()
Minimization of the tracking error#
builder = Builder(prices=prices, initial_aum=1e6)
for _, state in builder:
assets = state.assets
n = len(assets)
weights = cp.Variable(n)
objective = cp.norm(weights - np.ones(n)/n, 2)
constraints = [weights >= 0, cp.sum(weights) == 1]
cp.Problem(objective=cp.Minimize(objective), constraints=constraints).solve(solver=cp.CLARABEL)
# update weights & aum as before
builder.weights = weights.value
builder.aum = state.aum
portfolio = builder.build()
portfolio.snapshot(aggregate=True)
With sparse updates#
In practice we do not want to rebalance the portfolio every day. We tolerate our portfolio is not an exact \(1/n\) portfolio. We may expect slightly weaker results
builder = Builder(prices=prices, initial_aum=1e6)
for _, state in builder:
# assets currently alive, e.g. with a valid price
assets = state.assets
# number of assets currently alive
n = len(assets)
# Assets may drop out...
target = np.ones(n) / n
# the drifted weights for all valid assets
drifted = state.weights[assets].fillna(0.0)
# the delta is the sum of absolute weight changes
delta = (target - drifted).abs().sum()
if delta > 0.20:
# update the weights of the portfolio, e.g.
# rebalance it and set it all back to 1/n
builder.weights = target
else:
# forward-fill the position
builder.position = state.position
# or
# forward-fill the weights
# builder.weights = drifted
# or
# forward-filil the cashposition
# builider.cashposition = state.cashposition
# update the aum. Before you do that, you have the chance to correct it
# using your estimated trading costs, etc.
builder.aum = state.aum
portfolio = builder.build()
portfolio.snapshot(aggregate=True)